package com.lifeonwalden.completionsuggester.common;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class SuggesterNode<T> {
    private List<T> items;
    private List<T> tmpItems;
    private List<T> suggestions;
    private AtomicBoolean cacheReady = new AtomicBoolean(false);
    private Map<String, NodeInfo> exists;
    private HashMap<Integer, SuggesterNode> nextNodes;
    private Config config;

    public SuggesterNode(Config config) {
        this.items = new ArrayList<>();
        this.nextNodes = new HashMap<>();
        this.exists = new HashMap<>();
        this.config = config;
    }

    public void remove(T item, char[] alphabet, int startIdx) {
        String key = this.config.getSuggestItemIdFetcher().fetch(item);
        if (this.exists.containsKey(key)) {
            for (int i = 0; i < this.items.size(); i++) {
                if (this.config.getSuggestItemIdFetcher().fetch(this.items.get(i)).equals(key)) {
                    this.items.remove(i);
                    break;
                }
            }

            this.exists.remove(key);
        }

        int nextStartIdx = startIdx + 1;
        if (alphabet.length > nextStartIdx) {
            int codePoint = alphabet[nextStartIdx];
            SuggesterNode suggesterNode = this.nextNodes.get(codePoint);
            if (null != suggesterNode) {
                suggesterNode.remove(item, alphabet, nextStartIdx);
            }
        }

        this.cacheReady.set(false);
    }

    public void clearEmptyNode() {
        for (Integer key : this.nextNodes.keySet().toArray(new Integer[0])) {
            SuggesterNode node = this.nextNodes.get(key);
            if (null == node.tmpItems || node.tmpItems.size() == 0) {
                this.nextNodes.remove(key);
            } else {
                node.clearEmptyNode();
            }
        }
    }

    public void resize(long flushStamp) {
        this.items = this.tmpItems;
        this.tmpItems = null;

        for (String key : this.exists.keySet().toArray(new String[0])) {
            if (this.exists.get(key).getUpdateStamp() != flushStamp) {
                this.exists.remove(key);
            }
        }

        this.nextNodes.values().forEach(suggesterNode -> suggesterNode.resize(flushStamp));
        this.cacheReady.set(false);
    }

    public void refresh(T item, char[] alphabet, int startIdx, long flushStamp) {
        if (null == this.tmpItems) {
            this.tmpItems = new ArrayList<>();
        }
        String key = this.config.getSuggestItemIdFetcher().fetch(item);
        NodeInfo nodeInfo = this.exists.get(key);
        if (null == nodeInfo) {
            this.exists.put(key, new NodeInfo().setUpdateStamp(flushStamp));
            this.tmpItems.add(item);
        } else {
            if (nodeInfo.getUpdateStamp() != flushStamp) {
                this.tmpItems.add(item);
                nodeInfo.setUpdateStamp(flushStamp);
            }
        }

        int nextStartIdx = startIdx + 1;
        if (alphabet.length > nextStartIdx) {
            int codePoint = alphabet[nextStartIdx];
            SuggesterNode suggesterNode = this.nextNodes.get(codePoint);
            if (null == suggesterNode) {
                suggesterNode = new SuggesterNode(this.config);
                this.nextNodes.put(codePoint, suggesterNode);
            }

            suggesterNode.refresh(item, alphabet, nextStartIdx, flushStamp);
        }
    }

    public void add(T item, char[] alphabet, int startIdx) {
        String key = this.config.getSuggestItemIdFetcher().fetch(item);
        if (!this.exists.containsKey(key)) {
            this.exists.put(key, new NodeInfo());
            this.items.add(item);
        }

        int nextStartIdx = startIdx + 1;
        if (alphabet.length > nextStartIdx) {
            int codePoint = alphabet[nextStartIdx];
            SuggesterNode suggesterNode = this.nextNodes.get(codePoint);
            if (null == suggesterNode) {
                suggesterNode = new SuggesterNode(this.config);
                this.nextNodes.put(codePoint, suggesterNode);
            }

            suggesterNode.add(item, alphabet, nextStartIdx);
        }

        this.cacheReady.set(false);
    }

    public List<T> suggest(String question, int startIdx) {
        if (null == question || question.length() <= startIdx) {
            if (!this.cacheReady.get()) {
                this.suggestions = this.items.subList(0, this.config.getMaxSuggestItemCount() > this.items.size() ? this.items.size() : this.config.getMaxSuggestItemCount());
                this.cacheReady.set(true);
            }

            return this.suggestions;
        }

        SuggesterNode suggesterNode = this.nextNodes.get(question.codePointAt(startIdx));
        if (null == suggesterNode) {
            return Collections.emptyList();
        } else {
            return suggesterNode.suggest(question, startIdx + 1);
        }
    }
}
